package fregat.base.net;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * Implementation of base telegram class for binary telegrams
 * using @link java.nio.ByteBuffer.
 */
public class Telegram {
  private ByteBuffer buffer; // buffer that wraps the byte array
  private static final int HEX = 0;
  private static final int BIN = 1;

  /**
   * Creates an empty telegram without initialization.
   */
  protected Telegram() {}

  /**
   * Creates a telegram wrapper that uses a subsequence of the given binary data.
   * @param index Start of subsequence
   * @param length Length of subsequence
   * @param b Binary data
   */
  public Telegram(int index, int length, byte[] b) {
    wrap(index,length,b);
  }

  /**
   * Creates a telegram wrapper that wraps the given binary data.
   * @param b Binary data
   */
  public Telegram(byte[] b) {
    buffer = ByteBuffer.wrap(b);
  }

  /**
   * Creates an empty telegram wrapper that allocates binary data with the given length.
   * @param length Length of data in bytes
   */
  public Telegram(int length) {
    buffer = ByteBuffer.allocate(length);
  }

  /**
   * Wraps a subsequence of the given binary data.
   * @param index Start of subsequence
   * @param length Length of subsequence
   * @param b Binary data
   */
  protected void wrap(int index,int length,byte[] b) {
    byte[] sequence = new byte[length];
    System.arraycopy(b, index, sequence, 0, Math.min(length,b.length));
    buffer = ByteBuffer.wrap(sequence);
  }

  /**
   * Wraps the given binary data.
   * @param b Binary data
   */
  protected void wrap(byte[] b) {
    buffer = ByteBuffer.wrap(b);
  }

  /**
   * Adds the given binary data at the end of the existing data.
   * @param b Binary data
   */
  public void add(byte[] b) {
    insert(buffer.array().length,b);
  }

  /**
   * Inserts the given binary data at the specified index and shifts existing data.
   * @param index Position of data
   * @param b Binary data
   */
  public void insert(int index, byte[] b) {
    byte[] data = new byte[b.length+buffer.array().length];
    System.arraycopy(buffer.array(),0,data,0,index);
    System.arraycopy(buffer.array(),index,data,index+b.length,buffer.array().length-index);
    System.arraycopy(b,0,data,index,b.length);
    buffer = ByteBuffer.wrap(data);
  }


  /**
   * Sets the byte order for the telegram.
   * @param order Use <code>ByteOrder.LITTLE_ENDIAN</code>(default) or <code>ByteOrder.BIG_ENDIAN</code>
   */
  protected void order(ByteOrder order) {
    buffer.order(order);
  }

  /**
   * Sets the given text at the specified index.
   * @param index Byte number where text starts
   * @param value Text
   */
  public void setString(int index,String value) {
    for (int i = 0; i < value.length(); i++) {
      setChar(index+i,value.charAt(i));
    }
  }

  /**
   * Returns a string representation of the binary data using the given limits.
   * @param index Byte number where text starts
   * @param length Length of text in characters
   * @return Text
   */
  public String getString(int index,int length) {
    return new String(buffer.array(),index,length).trim();
  }

  /**
   * Sets the given character as 1 byte value at the specified byte.
   * @param index Byte number
   * @param value Character
   */
  public void setChar(int index,char value) {
    buffer.put(index,(byte)(value & 0xff));
  }

  /**
   * Returns the character representation of the specified byte.
   * @param index Byte number
   * @return Character
   */
  public char getChar(int index) {
    return (char)buffer.get(index);
  }

  /**
   * Sets the given integer as 1 byte value at the specified byte
   * @param index Byte number
   * @param value Integer (uint8)
   */
  public void setNum(int index,int value) {
    buffer.put(index,(byte)(value & 0xFF));
  }

  /**
   * Returns the 1 byte integer representation of the specified byte.
   * @param index Byte number
   * @return Integer (uint8)
   */
  public int getNum(int index) {
    return (int)(buffer.get(index) & 0xFF);
  }

  /**
   * Sets or resets the given bit of the specified byte.
   * @param index Byte number
   * @param bit Bit number
   * @param value True, to set
   */
  public void setBit(int index, int bit, boolean value) {
    byte b = buffer.get(index);
    if (value) {
      b |= (1 << bit);
    } else {
      b &= ~(1 << bit);
    }
    buffer.put(index,b);
  }

  /**
   * Returns the value of the given bit of the specified byte.
   * @param index Byte number
   * @param bit Bit number
   * @return True, if set
   */
  public boolean getBit(int index,int bit) {
    int value = buffer.get(index) & (1 << bit);
    return value != 0;
  }

  /**
   * Sets the given integer as 2 byte value starting at the specified byte.
   * @param index Byte number
   * @param value Integer (int16)
   */
  public void setWord(int index,int value) {
    buffer.putShort(index,(short)value);
  }

  /**
   * Returns a 2 byte integer representation starting at the specified byte.
   * @param index
   * @return Integer (int16)
   */
  public int getWord(int index) {
    return (int)buffer.getShort(index);
  }

  /**
   * Sets the given integer as 4 byte value starting at the specified byte.
   * @param index Byte number
   * @param value Integer (int32)
   */
  public void setDWord(int index, int value) {
    buffer.putInt(index,value);
  }

  /**
   * Returns a 4 byte integer representation starting at the specified byte.
   * @param index Byte number
   * @return Integer (int32)
   */
  public int getDWord(int index) {
    return buffer.getInt(index);
  }

  /**
   * Compares the given telegram to this instance.
   *
   * @param telegram Telegram to compare to
   * @return True if all bytes are equal
   */
  public boolean equals(Telegram telegram) {
    return buffer.equals(telegram.buffer);
  }

  /**
   * @return Length of telegram in bytes
   */
  public int getLength() {
    return buffer.array().length;
  }

  /**
   * @return Binary telegram data
   */
  public byte[] getBytes() {
    return buffer.array();
  }

  /**
   * Hexadecimal string representation of the given binary data.
   * @param b Binary data
   * @return Hexadecimal representation (eg. 0:5Bh 1:4Ch ...)
   */
  public static String toHexString(byte[] b) {
    return toString(HEX,ByteBuffer.wrap(b));
  }

  /**
   * Binary string representation of the given binary data.
   * @param b Binary data
   * @return Binary representation (eg. 0:00010000 1:00000010 ...)
   */
  public static String toBinaryString(byte[] b) {
    return toString(BIN,ByteBuffer.wrap(b));
  }

  /**
   * Hexadecimal string representation of the wrapped binary data.
   * @return Hexadecimal representation (eg. 0:5Bh 1:4Ch ...)
   */
  public String toHexString() {
    return toString(HEX,buffer);
  }

  /**
   * Binary string representation of the wrapped binary data.
   * @return Binary representation (eg. 0:00010000 1:00000010 ...)
   */
  public String toBinaryString() {
    return toString(BIN,buffer);
  }

  /**
   * Creates the binary or hexadecimal string representation.
   * @param type Representation type (use BIN or HEX)
   * @param buffer Buffer that wraps binary data
   * @return String representation
   */
  private static String toString(int type, ByteBuffer buffer) {
    StringBuffer strBuffer = new StringBuffer();

    for (int i = 0; i < buffer.array().length; i++) {
      strBuffer.append(i + ":");
      switch(type) {
        case BIN:
         strBuffer.append(binchar(i,buffer));
         break;
        case HEX:
          strBuffer.append(hexchar(i,buffer));
          break;
      }
      strBuffer.append(" ");
    }

    return strBuffer.toString();
  }

  /**
   * Returns a hexadecimal string representation of the byte at the specified index
   * in the buffer.
   * @param index Byte number
   * @param buffer Buffer that wraps binary data
   * @return Hexadecimal representation (eg. B5h)
   */
  private static String hexchar(int index, ByteBuffer buffer) {
    return hexchar(buffer.get(index));
  }

  /**
   * Returns a hexadecimal string representation of the given integer value.
   * @param value Integer value (uint8)
   * @return Hexadecimal string representation
   */
  public static String hexchar(int value) {
    if(value < 0) {
      value += 256; // convert to unsigned int
    }
    if (value <= 15)
      return "0" + Integer.toHexString(value).toUpperCase()+"h";
    else
      return Integer.toHexString(value).toUpperCase()+"h";
  }

   /**
   * Returns a string with the binary bit display of the specified byte in the
   * given byte array
   *
   * @param index Byte number
   * @param buffer Buffer containing bytes
   * @return Binary string representation
   */
  public static String binchar(int index,ByteBuffer buffer) {
    int mask = 0x80;
    String str = "";
    for (int i = 0; i < 8; i++) {
      if ((buffer.get(index) & mask) != 0) {
        str = str + "1";
      } else {
        str = str + "0";
      }
      mask = mask / 2;
    }
    return str;
  }

  /**
   * Returns a string with the binary bit display of the byte at the specified index.
   * @param index Byte number
   * @return Hexadecimal representation (eg. B5h)
   */
  public String binchar(int index) {
    return binchar(index,buffer);
  }
}
